package org.tea.crawl.biz.domain.service.impl;

import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.tea.crawl.biz.common.CommonTools;
import org.tea.crawl.biz.domain.common.CookieInfo;
import org.tea.crawl.biz.domain.dto.CookiePropDto;
import org.tea.crawl.biz.domain.entity.CookieInfoEntity;
import org.tea.crawl.biz.domain.enums.CookieType;
import org.tea.crawl.biz.domain.enums.CookieUseStatus;
import org.tea.crawl.biz.domain.service.CookieService;
import org.tea.crawl.biz.mapper.CookieInfoMapper;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author lxh
 */
@Service("jdCookieService")
@Transactional
public class JDCookieService
        implements CookieService {

    private final static Logger log = LoggerFactory.getLogger(JDCookieService.class);

    // 用于标识 JD 特定 Cookie 的缓存
    private final static String JD_COOKIE_KEY = "JD_COOKIE_KEY";

    // 用于标识 JD Cookie 的分布式锁的 Key 模板
    private final static String JD_COOKIE_LOCK_KEY = "JD_COOKIE_LOCK_KEY";

    @Resource
    private CookieInfoMapper cookieInfoMapper;

    @Resource(name = "crawlRedisson")
    private RedissonClient redissonClient;

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public List<CookieInfo> createValidSingleCookie() {
        // 目前该接口的调用次数不会很多，因此使用粗粒度锁不会有很大影响
        redissonClient.getLock(JD_COOKIE_LOCK_KEY).lock();
        try {
            RMap<Object, Object> cookieMap = redissonClient.getMap(JD_COOKIE_KEY);
            if (!Objects.isNull(cookieMap)) {
                CookieInfoEntity cookieInfoEntity = findValidCookie(cookieMap);
                if (!Objects.isNull(cookieInfoEntity)) {
                    flushData(cookieInfoEntity, cookieMap);
                    return convertToCookieInfos(cookieInfoEntity.getCookieValue());
                }
            }

            // 由于 Redis 缓存中没有可用的 Cookie 数据，因此需要从数据库中进行查找
            Map<String, String> dbCookies = convertToMap(loadAllCookiesFromDB());
            CookieInfoEntity dbValidCookie = findValidCookie(dbCookies);
            cookieMap.putAll(dbCookies);

            flushData(dbValidCookie, cookieMap);

            return Objects.isNull(dbValidCookie) ? null : convertToCookieInfos(dbValidCookie.getCookieValue());
        } finally {
            redissonClient.getLock(JD_COOKIE_LOCK_KEY).unlockAsync();
        }
    }

    @Override
    public int releaseCookie(String cookieId) {
        redissonClient.getLock(JD_COOKIE_LOCK_KEY).lock();
        try {
            RMap<Object, Object> cookieMap = redissonClient.getMap(JD_COOKIE_KEY);

            // 如果此时的 Cookie 缓存依旧生效，则更新缓存
            if (!CollectionUtils.isEmpty(cookieMap) && cookieMap.containsKey(cookieId)) {
                String s = String.valueOf(cookieMap.get(cookieId));
                try {
                    CookieInfoEntity cookieInfoEntity = objectMapper.readValue(s, CookieInfoEntity.class);
                    cookieInfoEntity.setUseStatus(CookieUseStatus.UN_USED.code);

                    cookieMap.put(cookieId, objectMapper.writeValueAsString(cookieInfoEntity));
                } catch (JsonProcessingException e) {
                    log.error("更新 Cookie 使用状态缓存失败，因此会直接删除该缓存", e);
                    cookieMap.remove(cookieId);
                }
            }
            // 缓存更新结束

            // 无论是否更新缓存，都需要更新数据库
            CookieInfoEntity cookieInfo = cookieInfoMapper.selectById(cookieId);
            cookieInfo.setUseStatus(CookieUseStatus.UN_USED.code);
            return cookieInfoMapper.updateById(cookieInfo);
        } catch (Throwable t) {
            log.error("释放 Cookie 的过程中出现异常:", t);
            return -1;
        } finally {
            redissonClient.getLock(JD_COOKIE_LOCK_KEY).unlockAsync();
        }
    }

    // 刷新当前被获取的 Cookie 信息到缓存和数据库中
    private void flushData(CookieInfoEntity cookieInfoEntity, RMap<Object, Object> cookieMap) {
        if (Objects.isNull(cookieInfoEntity)) return;
        try {
            // 更新现有缓存中的 Cookie 使用状态
            cookieInfoEntity.setUseStatus(CookieUseStatus.USING.code);
            cookieInfoEntity.setLastAcquireTime(new Date());
            cookieMap.put(String.valueOf(cookieInfoEntity.getId()), objectMapper.writeValueAsString(cookieInfoEntity));

            // 更新数据库中的相关数据状态
            cookieInfoMapper.updateById(cookieInfoEntity);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    private CookieInfoEntity findValidCookie(Map<?, ?> cookieMap) {
        for (Map.Entry<?, ?> entry : cookieMap.entrySet()) {
            if (Objects.isNull(entry.getValue())) continue;
            String s = String.valueOf(entry.getValue());
            try {
                CookieInfoEntity cookieInfo = objectMapper.readValue(s, CookieInfoEntity.class);
                if (!CookieUseStatus.UN_USED.code.equals(cookieInfo.getUseStatus())) continue;

                return cookieInfo;
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }

    private Map<String, String> convertToMap(List<? extends CookieInfoEntity> cookieInfos) {
        if (CollectionUtils.isEmpty(cookieInfos)) return Maps.newHashMap();
        return cookieInfos.stream().collect(Collectors.toMap(v -> String.valueOf(v.getId()), v -> {
            try {
                return objectMapper.writeValueAsString(v);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }));
    }

    private List<CookieInfo> convertToCookieInfos(String cookieJson) {
        try {
            Map<?, ?> map = objectMapper.readValue(cookieJson, Map.class);
            List<CookieInfo> ans = Lists.newArrayList();
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                CookiePropDto.Builder builder = CookiePropDto.Builder.builder();
                builder.name(String.valueOf(entry.getKey()));
                if (entry.getValue() instanceof List) {
                    List<?> list = (List<?>) entry.getValue();
                    if (CollectionUtils.isEmpty(list)) continue;
                    if (!list.isEmpty()) builder.value(String.valueOf(list.get(0)));
                    if (list.size() > 1) builder.domain(String.valueOf(list.get(1)));
                    if (list.size() > 2) builder.path(String.valueOf(list.get(2)));
                    if (list.size() > 3 && !String.valueOf(list.get(3)).isEmpty()
                            && Character.isDigit(String.valueOf(list.get(3)).charAt(0))) {
                        builder.expireDate(CommonTools.parseDate(String.valueOf(list.get(3))));
                    }
                    if (list.size() > 5) builder.httpOnly("Y".equals(String.valueOf(list.get(5))));
                    if (list.size() > 6) builder.secure("Y".equals(String.valueOf(list.get(6))));
                }
                ans.add(builder.build());
            }
            return ans;
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    protected List<CookieInfoEntity> loadAllCookiesFromDB() {
        return cookieInfoMapper.selectList(
                Wrappers.lambdaQuery(CookieInfoEntity.class)
                        .eq(CookieInfoEntity::getCookieType, CookieType.JD_COOKIE.code)
        );
    }
}
